热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

词法|减号_用一个JavaScript四则运算解释器来理解编译原理

篇首语:本文由编程笔记#小编为大家整理,主要介绍了用一个JavaScript四则运算解释器来理解编译原理相关的知识,希望对你有一定的参考价值。在前面的课程中,

篇首语:本文由编程笔记#小编为大家整理,主要介绍了用一个Javascript四则运算解释器来理解编译原理相关的知识,希望对你有一定的参考价值。


在前面的课程中,我在 Javascript 和 CSS 的部分,多次提到了编译原理相关的知识。这一部分的知识,如果我们从编译原理“龙书”等正规的资料中学习,就会耗费掉不少的时间,所以我在这里设计了一个小实验,帮助你快速理解编译原理相关的知识。


1. 分析

按照编译原理相关的知识,我们来设计一下工作,这里我们分成几个步骤。

1. 定义四则运算:产出四则运算的词法定义和语法定义;

2. 词法分析:把输入的字符串流变成 token;

3. 语法分析:把 token 变成抽象语法树 AST;

4. 解释执行:后序遍历 AST,执行得出结果;



2. 定义四则运算

四则运算就是加减乘除四种运算,例如:

1 + 2 * 3

首先我们来定义词法,四则运算里面只有数字和运算符,所以定义很简单,但是我们还要注意空格和换行符,所以词法定义大概是下面这样的。

1. Token

(1). Number: 1 2 3 4 5 6 7 8 9 0 的组合

(2). Operator: + 、-、 *、 / 之一

2. Whitespace:

3. LineTerminator:

这里我们对空白和换行符没有任何的处理,所以词法分析阶段会直接丢弃。

接下来我们来定义语法,语法定义多数采用 BNF,但是其实大家写起来都是乱写的,比如 Javascript 标准里面就是一种跟 BNF 类似的自创语法。

不过语法定义的核心思想不会变,都是几种结构的组合产生一个新的结构,所以语法定义也叫语法产生式。

因为加减乘除有优先级,所以我们可以认为加法是由若干个乘法再由加号或者减号连接成的:

::=

::=

|<&#43;>
|<->

这种 BNF 的写法类似递归的原理&#xff0c;你可以理解一下&#xff0c;它表示一个列表。为了方便&#xff0c;我们把普通数字也得当成乘法的一种特例了。

::&#61;

|<*>
|

好了&#xff0c;这就是四则运算的定义了。



3. 词法分析&#xff1a;状态机

词法分析部分&#xff0c;我们把字符流变成 token 流。词法分析有两种方案&#xff0c;一种是状态机&#xff0c;一种是正则表达式&#xff0c;它们是等效的&#xff0c;选择你喜欢的就好&#xff0c;这里我都会你介绍一下状态机。

根据分析&#xff0c;我们可能产生四种输入元素&#xff0c;其中只有两种 token&#xff0c;我们状态机的第一个状态就是根据第一个输入字符来判断进入了哪种状态&#xff1a;

var token &#61; [];
const start &#61; char &#61;>
if(char &#61;&#61;&#61; &#39;1&#39;
|| char &#61;&#61;&#61; &#39;2&#39;
|| char &#61;&#61;&#61; &#39;3&#39;
|| char &#61;&#61;&#61; &#39;4&#39;
|| char &#61;&#61;&#61; &#39;5&#39;
|| char &#61;&#61;&#61; &#39;6&#39;
|| char &#61;&#61;&#61; &#39;7&#39;
|| char &#61;&#61;&#61; &#39;8&#39;
|| char &#61;&#61;&#61; &#39;9&#39;
|| char &#61;&#61;&#61; &#39;0&#39;
)
token.push(char);
return inNumber;

if(char &#61;&#61;&#61; &#39;&#43;&#39;
|| char &#61;&#61;&#61; &#39;-&#39;
|| char &#61;&#61;&#61; &#39;*&#39;
|| char &#61;&#61;&#61; &#39;/&#39;
)
emmitToken(char, char);
return start

if(char &#61;&#61;&#61; &#39; &#39;)
return start;

if(char &#61;&#61;&#61; &#39;\\r&#39;
|| char &#61;&#61;&#61; &#39;\\n&#39;
)
return start;

const inNumber &#61; char &#61;>
if(char &#61;&#61;&#61; &#39;1&#39;
|| char &#61;&#61;&#61; &#39;2&#39;
|| char &#61;&#61;&#61; &#39;3&#39;
|| char &#61;&#61;&#61; &#39;4&#39;
|| char &#61;&#61;&#61; &#39;5&#39;
|| char &#61;&#61;&#61; &#39;6&#39;
|| char &#61;&#61;&#61; &#39;7&#39;
|| char &#61;&#61;&#61; &#39;8&#39;
|| char &#61;&#61;&#61; &#39;9&#39;
|| char &#61;&#61;&#61; &#39;0&#39;
)
token.push(char);
return inNumber;
else
emmitToken("Number", token.join(""));
token &#61; [];
return start(char); // put back char

这个状态机非常简单&#xff0c;它只有两个状态&#xff0c;因为我们只有 Number 不是单字符的 token。

这里我的状态机实现是非常经典的方式&#xff1a;用函数表示状态&#xff0c;用 if 表示状态的迁移关系&#xff0c;用 return 值表示下一个状态。

下面我们来运行一下这个状态机试试看&#xff1a;

function emmitToken(type, value)
console.log(value);
var input &#61; "1024 &#43; 2 * 256"
var state &#61; start;
for(var c of input.split(&#39;&#39;))
state &#61; state(c);
state(Symbol(&#39;EOF&#39;))

运行后我们发现输出如下&#xff1a;

1024
&#43;
2
*
256

这是我们想要的答案。



4. 语法分析&#xff1a;LL

做完了词法分析&#xff0c;我们开始进行语法分析&#xff0c;LL 语法分析根据每一个产生式来写一个函数&#xff0c;首先我们来写好函数名&#xff1a;

function AdditiveExpression( )
function MultiplicativeExpression()

为了便于理解&#xff0c;我们就不做流式处理了&#xff0c;实际上一般编译代码都应该支持流式处理。

所以我们假设 token 已经都拿到了&#xff1a;

var tokens &#61; [
type:"Number",
value: "1024"
,
type:"&#43;"
value: "&#43;"
,
type:"Number",
value: "2"
,
type:"*"
value: "*"
,
type:"Number",
value: "256"
,
type:"EOF"
];

每个产生式对应着一个函数&#xff0c;例如&#xff1a;根据产生式&#xff0c;我们的 AdditiveExpression 需要处理三种情况&#xff1a;

::&#61;

|<&#43;>
|<->

AdditiveExpression 的写法是根传入的节点&#xff0c;利用产生式合成新的节点。

function AdditiveExpression(source)
if(source[0].type &#61;&#61;&#61; "MultiplicativeExpression")
let node &#61;
type:"AdditiveExpression",
children:[source[0]]

source[0] &#61; node;
return node;

if(source[0].type &#61;&#61;&#61; "AdditiveExpression" && source[1].type &#61;&#61;&#61; "&#43;")
let node &#61;
type:"AdditiveExpression",
operator:"&#43;",
children:[source.shift(), source.shift(), MultiplicativeExpression(source)]

source.unshift(node);

if(source[0].type &#61;&#61;&#61; "AdditiveExpression" && source[1].type &#61;&#61;&#61; "-")
let node &#61;
type:"AdditiveExpression",
operator:"-",
children:[source.shift(), source.shift(), MultiplicativeExpression(source)]

source.unshift(node);

那么下一步我们就把解析好的 token 传给我们的顶层处理函数 Expression。

Expression(tokens);

接下来&#xff0c;我们看 Expression 该怎么处理它。

我们 Expression 收到第一个 token&#xff0c;是个 Number&#xff0c;这个时候&#xff0c;Expression 就傻了&#xff0c;这是因为产生式只告诉我们&#xff0c;收到了 AdditiveExpression 怎么办。

这个时候&#xff0c;我们就需要对产生式的首项层层展开&#xff0c;根据所有可能性调用相应的处理函数&#xff0c;这个过程在编译原理中称为求“closure”。


function Expression(source)
if(source[0].type &#61;&#61;&#61; "AdditiveExpression" && source[1] && source[1].type &#61;&#61;&#61; "EOF" )
let node &#61;
type:"Expression",
children:[source.shift(), source.shift()]

source.unshift(node);
return node;

AdditiveExpression(source);
return Expression(source);
function AdditiveExpression(source)
if(source[0].type &#61;&#61;&#61; "MultiplicativeExpression")
let node &#61;
type:"AdditiveExpression",
children:[source[0]]

source[0] &#61; node;
return AdditiveExpression(source);

if(source[0].type &#61;&#61;&#61; "AdditiveExpression" && source[1] && source[1].type &#61;&#61;&#61; "&#43;")
let node &#61;
type:"AdditiveExpression",
operator:"&#43;",
children:[]

node.children.push(source.shift());
node.children.push(source.shift());
MultiplicativeExpression(source);
node.children.push(source.shift());
source.unshift(node);
return AdditiveExpression(source);

if(source[0].type &#61;&#61;&#61; "AdditiveExpression" && source[1] && source[1].type &#61;&#61;&#61; "-")
let node &#61;
type:"AdditiveExpression",
operator:"-",
children:[]

node.children.push(source.shift());
node.children.push(source.shift());
MultiplicativeExpression(source);
node.children.push(source.shift());
source.unshift(node);
return AdditiveExpression(source);

if(source[0].type &#61;&#61;&#61; "AdditiveExpression")
return source[0];
MultiplicativeExpression(source);
return AdditiveExpression(source);
function MultiplicativeExpression(source)
if(source[0].type &#61;&#61;&#61; "Number")
let node &#61;
type:"MultiplicativeExpression",
children:[source[0]]

source[0] &#61; node;
return MultiplicativeExpression(source);

if(source[0].type &#61;&#61;&#61; "MultiplicativeExpression" && source[1] && source[1].type &#61;&#61;&#61; "*")
let node &#61;
type:"MultiplicativeExpression",
operator:"*",
children:[]

node.children.push(source.shift());
node.children.push(source.shift());
node.children.push(source.shift());
source.unshift(node);
return MultiplicativeExpression(source);

if(source[0].type &#61;&#61;&#61; "MultiplicativeExpression"&& source[1] && source[1].type &#61;&#61;&#61; "/")
let node &#61;
type:"MultiplicativeExpression",
operator:"/",
children:[]

node.children.push(source.shift());
node.children.push(source.shift());
node.children.push(source.shift());
source.unshift(node);
return MultiplicativeExpression(source);

if(source[0].type &#61;&#61;&#61; "MultiplicativeExpression")
return source[0];
return MultiplicativeExpression(source);
;
var source &#61; [
type:"Number",
value: "3"
,
type:"*",
value: "*"
,
type:"Number",
value: "300"
,
type:"&#43;",
value: "&#43;"
,
type:"Number",
value: "2"
,
type:"*",
value: "*"
,
type:"Number",
value: "256"
,
type:"EOF"
];
var ast &#61; Expression(source);
console.log(ast);


5. 解释执行

得到了 AST 之后&#xff0c;最困难的一步我们已经解决了。这里我们就不对这颗树做任何的优化和精简了&#xff0c;那么接下来&#xff0c;直接进入执行阶段。我们只需要对这个树做遍历操作执行即可。

我们根据不同的节点类型和其它信息&#xff0c;写 if 分别处理即可&#xff1a;

function evaluate(node)
if(node.type &#61;&#61;&#61; "Expression")
return evaluate(node.children[0])

if(node.type &#61;&#61;&#61; "AdditiveExpression")
if(node.operator &#61;&#61;&#61; &#39;-&#39;)
return evaluate(node.children[0]) - evaluate(node.children[2]);

if(node.operator &#61;&#61;&#61; &#39;&#43;&#39;)
return evaluate(node.children[0]) &#43; evaluate(node.children[2]);

return evaluate(node.children[0])

if(node.type &#61;&#61;&#61; "MultiplicativeExpression")
if(node.operator &#61;&#61;&#61; &#39;*&#39;)
return evaluate(node.children[0]) * evaluate(node.children[2]);

if(node.operator &#61;&#61;&#61; &#39;/&#39;)
return evaluate(node.children[0]) / evaluate(node.children[2]);

return evaluate(node.children[0])

if(node.type &#61;&#61;&#61; "Number")
return Number(node.value);



6. 总结

在这个小实验中&#xff0c;我们通过一个小实验学习了编译原理的基本知识&#xff0c;小实验的目的是帮助你理解 Javascript 课程中涉及到的编译原理基本概念&#xff0c;它离真正的编译原理学习还有很大的差距。

通过实验&#xff0c;我们了解了产生式、词法分析、语法分析和解释执行的过程。


推荐阅读
  • 本文介绍了设计师伊振华受邀参与沈阳市智慧城市运行管理中心项目的整体设计,并以数字赋能和创新驱动高质量发展的理念,建设了集成、智慧、高效的一体化城市综合管理平台,促进了城市的数字化转型。该中心被称为当代城市的智能心脏,为沈阳市的智慧城市建设做出了重要贡献。 ... [详细]
  • Python正则表达式学习记录及常用方法
    本文记录了学习Python正则表达式的过程,介绍了re模块的常用方法re.search,并解释了rawstring的作用。正则表达式是一种方便检查字符串匹配模式的工具,通过本文的学习可以掌握Python中使用正则表达式的基本方法。 ... [详细]
  • 前景:当UI一个查询条件为多项选择,或录入多个条件的时候,比如查询所有名称里面包含以下动态条件,需要模糊查询里面每一项时比如是这样一个数组条件:newstring[]{兴业银行, ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 本文介绍了深入浅出Linux设备驱动编程的重要性,以及两种加载和删除Linux内核模块的方法。通过一个内核模块的例子,展示了模块的编译和加载过程,并讨论了模块对内核大小的控制。深入理解Linux设备驱动编程对于开发者来说非常重要。 ... [详细]
  • 深度学习中的Vision Transformer (ViT)详解
    本文详细介绍了深度学习中的Vision Transformer (ViT)方法。首先介绍了相关工作和ViT的基本原理,包括图像块嵌入、可学习的嵌入、位置嵌入和Transformer编码器等。接着讨论了ViT的张量维度变化、归纳偏置与混合架构、微调及更高分辨率等方面。最后给出了实验结果和相关代码的链接。本文的研究表明,对于CV任务,直接应用纯Transformer架构于图像块序列是可行的,无需依赖于卷积网络。 ... [详细]
  • 本文介绍了在处理不规则数据时如何使用Python自动提取文本中的时间日期,包括使用dateutil.parser模块统一日期字符串格式和使用datefinder模块提取日期。同时,还介绍了一段使用正则表达式的代码,可以支持中文日期和一些特殊的时间识别,例如'2012年12月12日'、'3小时前'、'在2012/12/13哈哈'等。 ... [详细]
  • 本文由编程笔记#小编为大家整理,主要介绍了logistic回归(线性和非线性)相关的知识,包括线性logistic回归的代码和数据集的分布情况。希望对你有一定的参考价值。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • Linux环境变量函数getenv、putenv、setenv和unsetenv详解
    本文详细解释了Linux中的环境变量函数getenv、putenv、setenv和unsetenv的用法和功能。通过使用这些函数,可以获取、设置和删除环境变量的值。同时给出了相应的函数原型、参数说明和返回值。通过示例代码演示了如何使用getenv函数获取环境变量的值,并打印出来。 ... [详细]
  • 本文介绍了一个题目的解法,通过二分答案来解决问题,但困难在于如何进行检查。文章提供了一种逃逸方式,通过移动最慢的宿管来锁门时跑到更居中的位置,从而使所有合格的寝室都居中。文章还提到可以分开判断两边的情况,并使用前缀和的方式来求出在任意时刻能够到达宿管即将锁门的寝室的人数。最后,文章提到可以改成O(n)的直接枚举来解决问题。 ... [详细]
  • 本文介绍了Windows操作系统的版本及其特点,包括Windows 7系统的6个版本:Starter、Home Basic、Home Premium、Professional、Enterprise、Ultimate。Windows操作系统是微软公司研发的一套操作系统,具有人机操作性优异、支持的应用软件较多、对硬件支持良好等优点。Windows 7 Starter是功能最少的版本,缺乏Aero特效功能,没有64位支持,最初设计不能同时运行三个以上应用程序。 ... [详细]
  • 本文讨论了在手机移动端如何使用HTML5和JavaScript实现视频上传并压缩视频质量,或者降低手机摄像头拍摄质量的问题。作者指出HTML5和JavaScript无法直接压缩视频,只能通过将视频传送到服务器端由后端进行压缩。对于控制相机拍摄质量,只有使用JAVA编写Android客户端才能实现压缩。此外,作者还解释了在交作业时使用zip格式压缩包导致CSS文件和图片音乐丢失的原因,并提供了解决方法。最后,作者还介绍了一个用于处理图片的类,可以实现图片剪裁处理和生成缩略图的功能。 ... [详细]
  • 本文介绍了如何使用Express App提供静态文件,同时提到了一些不需要使用的文件,如package.json和/.ssh/known_hosts,并解释了为什么app.get('*')无法捕获所有请求以及为什么app.use(express.static(__dirname))可能会提供不需要的文件。 ... [详细]
  • Ihaveaworkfolderdirectory.我有一个工作文件夹目录。holderDir.glob(*)>holder[ProjectOne, ... [详细]
author-avatar
西安黄河文化补习学校
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有